#include <test/jtx.h>

#include <xrpld/app/consensus/RCLValidations.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/misc/NegativeUNLVote.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/app/tx/apply.h>

#include <xrpl/beast/unit_test.h>
#include <xrpl/ledger/View.h>

namespace ripple {
namespace test {

/*
 * This file implements the following negative UNL related tests:
 * -- test filling and applying ttUNL_MODIFY Tx and ledger update
 * -- test the NegativeUNLVote class. The test cases are split to multiple
 *    test classes to allow parallel execution.
 * -- test the negativeUNLFilter function
 *
 * Other negative UNL related tests such as ValidatorList and RPC related ones
 * are put in their existing unit test files.
 */

/**
 * Test the size of the negative UNL in a ledger,
 * also test if the ledger has ToDisalbe and/or ToReEnable
 *
 * @param l the ledger
 * @param size the expected negative UNL size
 * @param hasToDisable if expect ToDisable in ledger
 * @param hasToReEnable if expect ToDisable in ledger
 * @return true if meet all three expectation
 */
bool
negUnlSizeTest(
    std::shared_ptr<Ledger const> const& l,
    size_t size,
    bool hasToDisable,
    bool hasToReEnable);

/**
 * Try to apply a ttUNL_MODIFY Tx, and test the apply result
 *
 * @param env the test environment
 * @param view the OpenView of the ledger
 * @param tx the ttUNL_MODIFY Tx
 * @param pass if the Tx should be applied successfully
 * @return true if meet the expectation of apply result
 */
bool
applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass);

/**
 * Verify the content of negative UNL entries (public key and ledger sequence)
 * of a ledger
 *
 * @param l the ledger
 * @param nUnlLedgerSeq the expected PublicKeys and ledger Sequences
 * @note nUnlLedgerSeq is copied so that it can be modified.
 * @return true if meet the expectation
 */
bool
VerifyPubKeyAndSeq(
    std::shared_ptr<Ledger const> const& l,
    hash_map<PublicKey, std::uint32_t> nUnlLedgerSeq);

/**
 * Count the number of Tx in a TxSet
 *
 * @param txSet the TxSet
 * @return the number of Tx
 */
std::size_t
countTx(std::shared_ptr<SHAMap> const& txSet);

/**
 * Create fake public keys
 *
 * @param n the number of public keys
 * @return a vector of public keys created
 */
std::vector<PublicKey>
createPublicKeys(std::size_t n);

/**
 * Create ttUNL_MODIFY Tx
 *
 * @param disabling disabling or re-enabling a validator
 * @param seq current ledger seq
 * @param txKey the public key of the validator
 * @return the ttUNL_MODIFY Tx
 */
STTx
createTx(bool disabling, LedgerIndex seq, PublicKey const& txKey);

class NegativeUNL_test : public beast::unit_test::suite
{
    /**
     * Test filling and applying ttUNL_MODIFY Tx, as well as ledger update:
     *
     * We will build a long history of ledgers, and try to apply different
     * ttUNL_MODIFY Txes. We will check if the apply results meet expectations
     * and if the ledgers are updated correctly.
     */
    void
    testNegativeUNL()
    {
        /*
         * test cases:
         *
         * (1) the ledger after genesis
         * -- cannot apply Disable Tx
         * -- cannot apply ReEnable Tx
         * -- nUNL empty
         * -- no ToDisable
         * -- no ToReEnable
         *
         * (2) a flag ledger
         * -- apply an Disable Tx
         * -- cannot apply the second Disable Tx
         * -- cannot apply a ReEnable Tx
         * -- nUNL empty
         * -- has ToDisable with right nodeId
         * -- no ToReEnable
         * ++ extra test: first Disable Tx in ledger TxSet
         *
         * (3) ledgers before the next flag ledger
         * -- nUNL empty
         * -- has ToDisable with right nodeId
         * -- no ToReEnable
         *
         * (4) next flag ledger
         * -- nUNL size == 1, with right nodeId
         * -- no ToDisable
         * -- no ToReEnable
         * -- cannot apply an Disable Tx with nodeId already in nUNL
         * -- apply an Disable Tx with different nodeId
         * -- cannot apply a ReEnable Tx with the same NodeId as Add
         * -- cannot apply a ReEnable Tx with a NodeId not in nUNL
         * -- apply a ReEnable Tx with a nodeId already in nUNL
         * -- has ToDisable with right nodeId
         * -- has ToReEnable with right nodeId
         * -- nUNL size still 1, right nodeId
         *
         * (5) ledgers before the next flag ledger
         * -- nUNL size == 1, right nodeId
         * -- has ToDisable with right nodeId
         * -- has ToReEnable with right nodeId
         *
         * (6) next flag ledger
         * -- nUNL size == 1, different nodeId
         * -- no ToDisable
         * -- no ToReEnable
         * -- apply an Disable Tx with different nodeId
         * -- nUNL size still 1, right nodeId
         * -- has ToDisable with right nodeId
         * -- no ToReEnable
         *
         * (7) ledgers before the next flag ledger
         * -- nUNL size still 1, right nodeId
         * -- has ToDisable with right nodeId
         * -- no ToReEnable
         *
         * (8) next flag ledger
         * -- nUNL size == 2
         * -- apply a ReEnable Tx
         * -- cannot apply second ReEnable Tx, even with right nodeId
         * -- cannot apply an Disable Tx with the same NodeId as Remove
         * -- nUNL size == 2
         * -- no ToDisable
         * -- has ToReEnable with right nodeId
         *
         * (9) ledgers before the next flag ledger
         * -- nUNL size == 2
         * -- no ToDisable
         * -- has ToReEnable with right nodeId
         *
         * (10) next flag ledger
         * -- nUNL size == 1
         * -- apply a ReEnable Tx
         * -- nUNL size == 1
         * -- no ToDisable
         * -- has ToReEnable with right nodeId
         *
         * (11) ledgers before the next flag ledger
         * -- nUNL size == 1
         * -- no ToDisable
         * -- has ToReEnable with right nodeId
         *
         * (12) next flag ledger
         * -- nUNL size == 0
         * -- no ToDisable
         * -- no ToReEnable
         *
         * (13) ledgers before the next flag ledger
         * -- nUNL size == 0
         * -- no ToDisable
         * -- no ToReEnable
         *
         * (14) next flag ledger
         * -- nUNL size == 0
         * -- no ToDisable
         * -- no ToReEnable
         */

        testcase("Create UNLModify Tx and apply to ledgers");

        jtx::Env env(*this, jtx::testable_amendments());
        std::vector<PublicKey> publicKeys = createPublicKeys(3);
        // genesis ledger
        auto l = std::make_shared<Ledger>(
            create_genesis,
            env.app().config(),
            std::vector<uint256>{},
            env.app().getNodeFamily());

        // Record the public keys and ledger sequences of expected negative UNL
        // validators when we build the ledger history
        hash_map<PublicKey, std::uint32_t> nUnlLedgerSeq;

        {
            //(1) the ledger after genesis, not a flag ledger
            l = std::make_shared<Ledger>(
                *l, env.app().timeKeeper().closeTime());

            auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]);
            auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]);

            OpenView accum(&*l);
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false));
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false));
            accum.apply(*l);
            BEAST_EXPECT(negUnlSizeTest(l, 0, false, false));
        }

        {
            //(2) a flag ledger
            // generate more ledgers
            for (auto i = 0; i < 256 - 2; ++i)
            {
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]);
            auto txDisable_1 = createTx(true, l->seq(), publicKeys[1]);
            auto txReEnable_2 = createTx(false, l->seq(), publicKeys[2]);

            // can apply 1 and only 1 ToDisable Tx,
            // cannot apply ToReEnable Tx, since negative UNL is empty
            OpenView accum(&*l);
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, true));
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_1, false));
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_2, false));
            accum.apply(*l);
            auto good_size = negUnlSizeTest(l, 0, true, false);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
                //++ first ToDisable Tx in ledger's TxSet
                uint256 txID = txDisable_0.getTransactionID();
                BEAST_EXPECT(l->txExists(txID));
            }
        }

        {
            //(3) ledgers before the next flag ledger
            for (auto i = 0; i < 256; ++i)
            {
                auto good_size = negUnlSizeTest(l, 0, true, false);
                BEAST_EXPECT(good_size);
                if (good_size)
                    BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            //(4) next flag ledger
            // test if the ledger updated correctly
            auto good_size = negUnlSizeTest(l, 1, false, false);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(*(l->negativeUNL().begin()) == publicKeys[0]);
                nUnlLedgerSeq.emplace(publicKeys[0], l->seq());
            }

            auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]);
            auto txDisable_1 = createTx(true, l->seq(), publicKeys[1]);
            auto txReEnable_0 = createTx(false, l->seq(), publicKeys[0]);
            auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]);
            auto txReEnable_2 = createTx(false, l->seq(), publicKeys[2]);

            OpenView accum(&*l);
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false));
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_1, true));
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false));
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_2, false));
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_0, true));
            accum.apply(*l);
            good_size = negUnlSizeTest(l, 1, true, true);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[0]));
                BEAST_EXPECT(l->validatorToDisable() == publicKeys[1]);
                BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]);
                // test sfFirstLedgerSequence
                BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq));
            }
        }

        {
            //(5) ledgers before the next flag ledger
            for (auto i = 0; i < 256; ++i)
            {
                auto good_size = negUnlSizeTest(l, 1, true, true);
                BEAST_EXPECT(good_size);
                if (good_size)
                {
                    BEAST_EXPECT(l->negativeUNL().count(publicKeys[0]));
                    BEAST_EXPECT(l->validatorToDisable() == publicKeys[1]);
                    BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]);
                }
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            //(6) next flag ledger
            // test if the ledger updated correctly
            auto good_size = negUnlSizeTest(l, 1, false, false);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
            }

            auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]);

            OpenView accum(&*l);
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, true));
            accum.apply(*l);
            good_size = negUnlSizeTest(l, 1, true, false);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
                nUnlLedgerSeq.emplace(publicKeys[1], l->seq());
                nUnlLedgerSeq.erase(publicKeys[0]);
                BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq));
            }
        }

        {
            //(7) ledgers before the next flag ledger
            for (auto i = 0; i < 256; ++i)
            {
                auto good_size = negUnlSizeTest(l, 1, true, false);
                BEAST_EXPECT(good_size);
                if (good_size)
                {
                    BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                    BEAST_EXPECT(l->validatorToDisable() == publicKeys[0]);
                }
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            //(8) next flag ledger
            // test if the ledger updated correctly
            auto good_size = negUnlSizeTest(l, 2, false, false);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[0]));
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                nUnlLedgerSeq.emplace(publicKeys[0], l->seq());
                BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq));
            }

            auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]);
            auto txReEnable_0 = createTx(false, l->seq(), publicKeys[0]);
            auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]);

            OpenView accum(&*l);
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_0, true));
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false));
            BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false));
            accum.apply(*l);
            good_size = negUnlSizeTest(l, 2, false, true);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[0]));
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]);
                BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq));
            }
        }

        {
            //(9) ledgers before the next flag ledger
            for (auto i = 0; i < 256; ++i)
            {
                auto good_size = negUnlSizeTest(l, 2, false, true);
                BEAST_EXPECT(good_size);
                if (good_size)
                {
                    BEAST_EXPECT(l->negativeUNL().count(publicKeys[0]));
                    BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                    BEAST_EXPECT(l->validatorToReEnable() == publicKeys[0]);
                }
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            //(10) next flag ledger
            // test if the ledger updated correctly
            auto good_size = negUnlSizeTest(l, 1, false, false);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                nUnlLedgerSeq.erase(publicKeys[0]);
                BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq));
            }

            auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]);

            OpenView accum(&*l);
            BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, true));
            accum.apply(*l);
            good_size = negUnlSizeTest(l, 1, false, true);
            BEAST_EXPECT(good_size);
            if (good_size)
            {
                BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                BEAST_EXPECT(l->validatorToReEnable() == publicKeys[1]);
                BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq));
            }
        }

        {
            //(11) ledgers before the next flag ledger
            for (auto i = 0; i < 256; ++i)
            {
                auto good_size = negUnlSizeTest(l, 1, false, true);
                BEAST_EXPECT(good_size);
                if (good_size)
                {
                    BEAST_EXPECT(l->negativeUNL().count(publicKeys[1]));
                    BEAST_EXPECT(l->validatorToReEnable() == publicKeys[1]);
                }
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            //(12) next flag ledger
            BEAST_EXPECT(negUnlSizeTest(l, 0, false, false));
        }

        {
            //(13) ledgers before the next flag ledger
            for (auto i = 0; i < 256; ++i)
            {
                BEAST_EXPECT(negUnlSizeTest(l, 0, false, false));
                l = std::make_shared<Ledger>(
                    *l, env.app().timeKeeper().closeTime());
            }
            BEAST_EXPECT(l->isFlagLedger());
            l->updateNegativeUNL();

            //(14) next flag ledger
            BEAST_EXPECT(negUnlSizeTest(l, 0, false, false));
        }
    }

    void
    run() override
    {
        testNegativeUNL();
    }
};

/**
 * Utility class for creating validators and ledger history
 */
struct NetworkHistory
{
    using LedgerHistory = std::vector<std::shared_ptr<Ledger>>;
    /**
     *
     * Only reasonable parameters can be honored,
     * e.g cannot hasToReEnable when nUNLSize == 0
     */
    struct Parameter
    {
        std::uint32_t numNodes;    // number of validators
        std::uint32_t negUNLSize;  // size of negative UNL in the last ledger
        bool hasToDisable;         // if has ToDisable in the last ledger
        bool hasToReEnable;        // if has ToReEnable in the last ledger
        /**
         * if not specified, the number of ledgers in the history is calculated
         * from negUNLSize, hasToDisable, and hasToReEnable
         */
        std::optional<int> numLedgers;
    };

    NetworkHistory(beast::unit_test::suite& suite, Parameter const& p)
        : env(suite, jtx::testable_amendments())
        , param(p)
        , validations(env.app().getValidations())
    {
        createNodes();
        if (!param.numLedgers)
            param.numLedgers = 256 * (param.negUNLSize + 1);
        goodHistory = createLedgerHistory();
    }

    void
    createNodes()
    {
        assert(param.numNodes <= 256);
        UNLKeys = createPublicKeys(param.numNodes);
        for (int i = 0; i < param.numNodes; ++i)
        {
            UNLKeySet.insert(UNLKeys[i]);
            UNLNodeIDs.push_back(calcNodeID(UNLKeys[i]));
            UNLNodeIDSet.insert(UNLNodeIDs.back());
        }
    }

    /**
     * create ledger history and apply needed ttUNL_MODIFY tx at flag ledgers
     * @return
     */
    bool
    createLedgerHistory()
    {
        static uint256 fake_amemdment;  // So we have different genesis ledgers
        auto l = std::make_shared<Ledger>(
            create_genesis,
            env.app().config(),
            std::vector<uint256>{fake_amemdment++},
            env.app().getNodeFamily());
        history.push_back(l);

        // When putting validators into the negative UNL, we start with
        // validator 0, then validator 1 ...
        int nidx = 0;
        while (l->seq() <= param.numLedgers)
        {
            l = std::make_shared<Ledger>(
                *l, env.app().timeKeeper().closeTime());
            history.push_back(l);

            if (l->isFlagLedger())
            {
                l->updateNegativeUNL();
                OpenView accum(&*l);
                if (l->negativeUNL().size() < param.negUNLSize)
                {
                    auto tx = createTx(true, l->seq(), UNLKeys[nidx]);
                    if (!applyAndTestResult(env, accum, tx, true))
                        break;
                    ++nidx;
                }
                else if (l->negativeUNL().size() == param.negUNLSize)
                {
                    if (param.hasToDisable)
                    {
                        auto tx = createTx(true, l->seq(), UNLKeys[nidx]);
                        if (!applyAndTestResult(env, accum, tx, true))
                            break;
                        ++nidx;
                    }
                    if (param.hasToReEnable)
                    {
                        auto tx = createTx(false, l->seq(), UNLKeys[0]);
                        if (!applyAndTestResult(env, accum, tx, true))
                            break;
                    }
                }
                accum.apply(*l);
            }
            l->updateSkipList();
        }
        return negUnlSizeTest(
            l, param.negUNLSize, param.hasToDisable, param.hasToReEnable);
    }

    /**
     * Create a validation
     * @param ledger the ledger the validation validates
     * @param v the validator
     * @return the validation
     */
    std::shared_ptr<STValidation>
    createSTVal(std::shared_ptr<Ledger const> const& ledger, NodeID const& v)
    {
        static auto keyPair = randomKeyPair(KeyType::secp256k1);
        return std::make_shared<STValidation>(
            env.app().timeKeeper().now(),
            keyPair.first,
            keyPair.second,
            v,
            [&](STValidation& v) {
                v.setFieldH256(sfLedgerHash, ledger->info().hash);
                v.setFieldU32(sfLedgerSequence, ledger->seq());
                v.setFlag(vfFullValidation);
            });
    };

    /**
     * Walk the ledger history and create validation messages for the ledgers
     *
     * @tparam NeedValidation a function to decided if a validation is needed
     * @param needVal if a validation is needed for this particular combination
     *        of ledger and validator
     */
    template <class NeedValidation>
    void
    walkHistoryAndAddValidations(NeedValidation&& needVal)
    {
        std::uint32_t curr = 0;
        std::size_t need = 256 + 1;
        // only last 256 + 1 ledgers need validations
        if (history.size() > need)
            curr = history.size() - need;
        for (; curr != history.size(); ++curr)
        {
            for (std::size_t i = 0; i < param.numNodes; ++i)
            {
                if (needVal(history[curr], i))
                {
                    RCLValidation v(createSTVal(history[curr], UNLNodeIDs[i]));
                    v.setTrusted();
                    validations.add(UNLNodeIDs[i], v);
                }
            }
        }
    }

    std::shared_ptr<Ledger const>
    lastLedger() const
    {
        return history.back();
    }

    jtx::Env env;
    Parameter param;
    RCLValidations& validations;
    std::vector<PublicKey> UNLKeys;
    hash_set<PublicKey> UNLKeySet;
    std::vector<NodeID> UNLNodeIDs;
    hash_set<NodeID> UNLNodeIDSet;
    LedgerHistory history;
    bool goodHistory;
};

auto defaultPreVote = [](NegativeUNLVote& vote) {};
/**
 * Create a NegativeUNLVote object. It then creates ttUNL_MODIFY Tx as its vote
 * on negative UNL changes.
 *
 * @tparam PreVote a function to be called before vote
 * @param history the ledger history
 * @param myId the voting validator
 * @param expect the number of ttUNL_MODIFY Tx expected
 * @param pre the PreVote function
 * @return true if the number of ttUNL_MODIFY Txes created meet expectation
 */
template <typename PreVote = decltype(defaultPreVote)>
bool
voteAndCheck(
    NetworkHistory& history,
    NodeID const& myId,
    std::size_t expect,
    PreVote const& pre = defaultPreVote)
{
    NegativeUNLVote vote(myId, history.env.journal);
    pre(vote);
    auto txSet = std::make_shared<SHAMap>(
        SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
    vote.doVoting(
        history.lastLedger(), history.UNLKeySet, history.validations, txSet);
    return countTx(txSet) == expect;
}

/**
 * Test the private member functions of NegativeUNLVote
 */
class NegativeUNLVoteInternal_test : public beast::unit_test::suite
{
    void
    testAddTx()
    {
        testcase("Create UNLModify Tx");
        jtx::Env env(*this);

        NodeID myId(0xA0);
        NegativeUNLVote vote(myId, env.journal);

        // one add, one remove
        auto txSet = std::make_shared<SHAMap>(
            SHAMapType::TRANSACTION, env.app().getNodeFamily());
        PublicKey toDisableKey(
            derivePublicKey(KeyType::ed25519, randomSecretKey()));
        PublicKey toReEnableKey(
            derivePublicKey(KeyType::ed25519, randomSecretKey()));
        LedgerIndex seq(1234);
        BEAST_EXPECT(countTx(txSet) == 0);
        vote.addTx(seq, toDisableKey, NegativeUNLVote::ToDisable, txSet);
        BEAST_EXPECT(countTx(txSet) == 1);
        vote.addTx(seq, toReEnableKey, NegativeUNLVote::ToReEnable, txSet);
        BEAST_EXPECT(countTx(txSet) == 2);
        // content of a tx is implicitly tested after applied to a ledger
        // in later test cases
    }

    void
    testPickOneCandidate()
    {
        testcase("Pick One Candidate");
        jtx::Env env(*this);

        NodeID myId(0xA0);
        NegativeUNLVote vote(myId, env.journal);

        uint256 pad_0(0);
        uint256 pad_f = ~pad_0;
        NodeID n_1(1);
        NodeID n_2(2);
        NodeID n_3(3);
        std::vector<NodeID> candidates({n_1});
        BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1);
        BEAST_EXPECT(vote.choose(pad_f, candidates) == n_1);
        candidates.emplace_back(2);
        BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1);
        BEAST_EXPECT(vote.choose(pad_f, candidates) == n_2);
        candidates.emplace_back(3);
        BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1);
        BEAST_EXPECT(vote.choose(pad_f, candidates) == n_3);
    }

    void
    testBuildScoreTableSpecialCases()
    {
        testcase("Build Score Table");
        /*
         * 1. no skip list
         * 2. short skip list
         * 3. local node not enough history
         * 4. a node double validated some seq
         * 5. local node had enough validations but on a wrong chain
         * 6. a good case, long enough history and perfect scores
         */
        {
            // 1. no skip list
            NetworkHistory history = {*this, {10, 0, false, false, 1}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                NegativeUNLVote vote(
                    history.UNLNodeIDs[3], history.env.journal);
                BEAST_EXPECT(!vote.buildScoreTable(
                    history.lastLedger(),
                    history.UNLNodeIDSet,
                    history.validations));
            }
        }

        {
            // 2. short skip list
            NetworkHistory history = {*this, {10, 0, false, false, 256 / 2}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                NegativeUNLVote vote(
                    history.UNLNodeIDs[3], history.env.journal);
                BEAST_EXPECT(!vote.buildScoreTable(
                    history.lastLedger(),
                    history.UNLNodeIDSet,
                    history.validations));
            }
        }

        {
            // 3. local node not enough history
            NetworkHistory history = {*this, {10, 0, false, false, 256 + 2}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                NodeID myId = history.UNLNodeIDs[3];
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool {
                        // skip half my validations.
                        return !(
                            history.UNLNodeIDs[idx] == myId &&
                            l->seq() % 2 == 0);
                    });
                NegativeUNLVote vote(myId, history.env.journal);
                BEAST_EXPECT(!vote.buildScoreTable(
                    history.lastLedger(),
                    history.UNLNodeIDSet,
                    history.validations));
            }
        }

        {
            // 4. a node double validated some seq
            // 5. local node had enough validations but on a wrong chain
            NetworkHistory history = {*this, {10, 0, false, false, 256 + 2}};
            // We need two chains for these tests
            bool wrongChainSuccess = history.goodHistory;
            BEAST_EXPECT(wrongChainSuccess);
            NetworkHistory::LedgerHistory wrongChain =
                std::move(history.history);
            // Create a new chain and use it as the one that majority of nodes
            // follow
            history.createLedgerHistory();
            BEAST_EXPECT(history.goodHistory);

            if (history.goodHistory && wrongChainSuccess)
            {
                NodeID myId = history.UNLNodeIDs[3];
                NodeID badNode = history.UNLNodeIDs[4];
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool {
                        // everyone but me
                        return !(history.UNLNodeIDs[idx] == myId);
                    });

                // local node validate wrong chain
                // a node double validates
                for (auto& l : wrongChain)
                {
                    RCLValidation v1(history.createSTVal(l, myId));
                    history.validations.add(myId, v1);
                    RCLValidation v2(history.createSTVal(l, badNode));
                    history.validations.add(badNode, v2);
                }

                NegativeUNLVote vote(myId, history.env.journal);

                // local node still on wrong chain, can build a scoreTable,
                // but all other nodes' scores are zero
                auto scoreTable = vote.buildScoreTable(
                    wrongChain.back(),
                    history.UNLNodeIDSet,
                    history.validations);
                BEAST_EXPECT(scoreTable);
                if (scoreTable)
                {
                    for (auto const& [n, score] : *scoreTable)
                    {
                        if (n == myId)
                            BEAST_EXPECT(score == 256);
                        else
                            BEAST_EXPECT(score == 0);
                    }
                }

                // if local node switched to right history, but cannot build
                // scoreTable because not enough local validations
                BEAST_EXPECT(!vote.buildScoreTable(
                    history.lastLedger(),
                    history.UNLNodeIDSet,
                    history.validations));
            }
        }

        {
            // 6. a good case
            NetworkHistory history = {*this, {10, 0, false, false, 256 + 1}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return true; });
                NegativeUNLVote vote(
                    history.UNLNodeIDs[3], history.env.journal);
                auto scoreTable = vote.buildScoreTable(
                    history.lastLedger(),
                    history.UNLNodeIDSet,
                    history.validations);
                BEAST_EXPECT(scoreTable);
                if (scoreTable)
                {
                    for (auto const& [_, score] : *scoreTable)
                    {
                        (void)_;
                        BEAST_EXPECT(score == 256);
                    }
                }
            }
        }
    }

    /**
     * Find all candidates and check if the number of candidates meets
     * expectation
     *
     * @param vote the NegativeUNLVote object
     * @param unl the validators
     * @param negUnl the negative UNL validators
     * @param scoreTable the score table of validators
     * @param numDisable number of Disable candidates expected
     * @param numReEnable number of ReEnable candidates expected
     * @return true if the number of candidates meets expectation
     */
    bool
    checkCandidateSizes(
        NegativeUNLVote& vote,
        hash_set<NodeID> const& unl,
        hash_set<NodeID> const& negUnl,
        hash_map<NodeID, std::uint32_t> const& scoreTable,
        std::size_t numDisable,
        std::size_t numReEnable)
    {
        auto [disableCandidates, reEnableCandidates] =
            vote.findAllCandidates(unl, negUnl, scoreTable);
        bool rightDisable = disableCandidates.size() == numDisable;
        bool rightReEnable = reEnableCandidates.size() == numReEnable;
        return rightDisable && rightReEnable;
    };

    void
    testFindAllCandidates()
    {
        testcase("Find All Candidates");
        /*
         * -- unl size: 35
         * -- negUnl size: 3
         *
         * 0. all good scores
         * 1. all bad scores
         * 2. all between watermarks
         * 3. 2 good scorers in negUnl
         * 4. 2 bad scorers not in negUnl
         * 5. 2 in negUnl but not in unl, have a remove candidate from score
         * table
         * 6. 2 in negUnl but not in unl, no remove candidate from score table
         * 7. 2 new validators have good scores, already in negUnl
         * 8. 2 new validators have bad scores, not in negUnl
         * 9. expired the new validators have bad scores, not in negUnl
         */
        NetworkHistory history = {*this, {35, 0, false, false, 0}};

        hash_set<NodeID> negUnl_012;
        for (std::uint32_t i = 0; i < 3; ++i)
            negUnl_012.insert(history.UNLNodeIDs[i]);

        // build a good scoreTable to use, or copy and modify
        hash_map<NodeID, std::uint32_t> goodScoreTable;
        for (auto const& n : history.UNLNodeIDs)
            goodScoreTable[n] = NegativeUNLVote::negativeUNLHighWaterMark + 1;

        NegativeUNLVote vote(history.UNLNodeIDs[0], history.env.journal);

        {
            // all good scores
            BEAST_EXPECT(checkCandidateSizes(
                vote, history.UNLNodeIDSet, negUnl_012, goodScoreTable, 0, 3));
        }
        {
            // all bad scores
            hash_map<NodeID, std::uint32_t> scoreTable;
            for (auto& n : history.UNLNodeIDs)
                scoreTable[n] = NegativeUNLVote::negativeUNLLowWaterMark - 1;
            BEAST_EXPECT(checkCandidateSizes(
                vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 35 - 3, 0));
        }
        {
            // all between watermarks
            hash_map<NodeID, std::uint32_t> scoreTable;
            for (auto& n : history.UNLNodeIDs)
                scoreTable[n] = NegativeUNLVote::negativeUNLLowWaterMark + 1;
            BEAST_EXPECT(checkCandidateSizes(
                vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 0, 0));
        }

        {
            // 2 good scorers in negUnl
            auto scoreTable = goodScoreTable;
            scoreTable[*negUnl_012.begin()] =
                NegativeUNLVote::negativeUNLLowWaterMark + 1;
            BEAST_EXPECT(checkCandidateSizes(
                vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 0, 2));
        }

        {
            // 2 bad scorers not in negUnl
            auto scoreTable = goodScoreTable;
            scoreTable[history.UNLNodeIDs[11]] =
                NegativeUNLVote::negativeUNLLowWaterMark - 1;
            scoreTable[history.UNLNodeIDs[12]] =
                NegativeUNLVote::negativeUNLLowWaterMark - 1;
            BEAST_EXPECT(checkCandidateSizes(
                vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 2, 3));
        }

        {
            // 2 in negUnl but not in unl, have a remove candidate from score
            // table
            hash_set<NodeID> UNL_temp = history.UNLNodeIDSet;
            UNL_temp.erase(history.UNLNodeIDs[0]);
            UNL_temp.erase(history.UNLNodeIDs[1]);
            BEAST_EXPECT(checkCandidateSizes(
                vote, UNL_temp, negUnl_012, goodScoreTable, 0, 3));
        }

        {
            // 2 in negUnl but not in unl, no remove candidate from score table
            auto scoreTable = goodScoreTable;
            scoreTable.erase(history.UNLNodeIDs[0]);
            scoreTable.erase(history.UNLNodeIDs[1]);
            scoreTable[history.UNLNodeIDs[2]] =
                NegativeUNLVote::negativeUNLLowWaterMark + 1;
            hash_set<NodeID> UNL_temp = history.UNLNodeIDSet;
            UNL_temp.erase(history.UNLNodeIDs[0]);
            UNL_temp.erase(history.UNLNodeIDs[1]);
            BEAST_EXPECT(checkCandidateSizes(
                vote, UNL_temp, negUnl_012, scoreTable, 0, 2));
        }

        {
            // 2 new validators
            NodeID new_1(0xbead);
            NodeID new_2(0xbeef);
            hash_set<NodeID> nowTrusted = {new_1, new_2};
            hash_set<NodeID> UNL_temp = history.UNLNodeIDSet;
            UNL_temp.insert(new_1);
            UNL_temp.insert(new_2);
            vote.newValidators(256, nowTrusted);
            {
                // 2 new validators have good scores, already in negUnl
                auto scoreTable = goodScoreTable;
                scoreTable[new_1] =
                    NegativeUNLVote::negativeUNLHighWaterMark + 1;
                scoreTable[new_2] =
                    NegativeUNLVote::negativeUNLHighWaterMark + 1;
                hash_set<NodeID> negUnl_temp = negUnl_012;
                negUnl_temp.insert(new_1);
                negUnl_temp.insert(new_2);
                BEAST_EXPECT(checkCandidateSizes(
                    vote, UNL_temp, negUnl_temp, scoreTable, 0, 3 + 2));
            }
            {
                // 2 new validators have bad scores, not in negUnl
                auto scoreTable = goodScoreTable;
                scoreTable[new_1] = 0;
                scoreTable[new_2] = 0;
                BEAST_EXPECT(checkCandidateSizes(
                    vote, UNL_temp, negUnl_012, scoreTable, 0, 3));
            }
            {
                // expired the new validators have bad scores, not in negUnl
                vote.purgeNewValidators(
                    256 + NegativeUNLVote::newValidatorDisableSkip + 1);
                auto scoreTable = goodScoreTable;
                scoreTable[new_1] = 0;
                scoreTable[new_2] = 0;
                BEAST_EXPECT(checkCandidateSizes(
                    vote, UNL_temp, negUnl_012, scoreTable, 2, 3));
            }
        }
    }

    void
    testFindAllCandidatesCombination()
    {
        testcase("Find All Candidates Combination");
        /*
         * == combination 1:
         * -- unl size: 34, 35, 80
         * -- nUnl size: 0, 50%, all
         * -- score pattern: all 0, all negativeUNLLowWaterMark & +1 & -1, all
         * negativeUNLHighWaterMark & +1 & -1, all 100%
         *
         * == combination 2:
         * -- unl size: 34, 35, 80
         * -- negativeUNL size: 0, all
         * -- nUnl size: one on, one off, one on, one off,
         * -- score pattern: 2*(negativeUNLLowWaterMark, +1, -1) &
         * 2*(negativeUNLHighWaterMark, +1, -1) & rest
         * negativeUNLMinLocalValsToVote
         */

        jtx::Env env(*this);

        NodeID myId(0xA0);
        NegativeUNLVote vote(myId, env.journal);

        std::array<std::uint32_t, 3> unlSizes = {34, 35, 80};
        std::array<std::uint32_t, 3> nUnlPercent = {0, 50, 100};
        std::array<std::uint32_t, 8> scores = {
            0,
            NegativeUNLVote::negativeUNLLowWaterMark - 1,
            NegativeUNLVote::negativeUNLLowWaterMark,
            NegativeUNLVote::negativeUNLLowWaterMark + 1,
            NegativeUNLVote::negativeUNLHighWaterMark - 1,
            NegativeUNLVote::negativeUNLHighWaterMark,
            NegativeUNLVote::negativeUNLHighWaterMark + 1,
            NegativeUNLVote::negativeUNLMinLocalValsToVote};

        //== combination 1:
        {
            auto fillScoreTable =
                [&](std::uint32_t unl_size,
                    std::uint32_t nUnl_size,
                    std::uint32_t score,
                    hash_set<NodeID>& unl,
                    hash_set<NodeID>& negUnl,
                    hash_map<NodeID, std::uint32_t>& scoreTable) {
                    std::vector<NodeID> nodeIDs;
                    std::vector<PublicKey> keys = createPublicKeys(unl_size);
                    for (auto const& k : keys)
                    {
                        nodeIDs.emplace_back(calcNodeID(k));
                        unl.emplace(nodeIDs.back());
                        scoreTable[nodeIDs.back()] = score;
                    }
                    for (std::uint32_t i = 0; i < nUnl_size; ++i)
                        negUnl.insert(nodeIDs[i]);
                };

            for (auto us : unlSizes)
            {
                for (auto np : nUnlPercent)
                {
                    for (auto score : scores)
                    {
                        hash_set<NodeID> unl;
                        hash_set<NodeID> negUnl;
                        hash_map<NodeID, std::uint32_t> scoreTable;
                        fillScoreTable(
                            us, us * np / 100, score, unl, negUnl, scoreTable);
                        BEAST_EXPECT(unl.size() == us);
                        BEAST_EXPECT(negUnl.size() == us * np / 100);
                        BEAST_EXPECT(scoreTable.size() == us);

                        std::size_t toDisable_expect = 0;
                        std::size_t toReEnable_expect = 0;
                        if (np == 0)
                        {
                            if (score <
                                NegativeUNLVote::negativeUNLLowWaterMark)
                            {
                                toDisable_expect = us;
                            }
                        }
                        else if (np == 50)
                        {
                            if (score >
                                NegativeUNLVote::negativeUNLHighWaterMark)
                            {
                                toReEnable_expect = us * np / 100;
                            }
                        }
                        else
                        {
                            if (score >
                                NegativeUNLVote::negativeUNLHighWaterMark)
                            {
                                toReEnable_expect = us;
                            }
                        }
                        BEAST_EXPECT(checkCandidateSizes(
                            vote,
                            unl,
                            negUnl,
                            scoreTable,
                            toDisable_expect,
                            toReEnable_expect));
                    }
                }
            }

            //== combination 2:
            {
                auto fillScoreTable =
                    [&](std::uint32_t unl_size,
                        std::uint32_t nUnl_percent,
                        hash_set<NodeID>& unl,
                        hash_set<NodeID>& negUnl,
                        hash_map<NodeID, std::uint32_t>& scoreTable) {
                        std::vector<NodeID> nodeIDs;
                        std::vector<PublicKey> keys =
                            createPublicKeys(unl_size);
                        for (auto const& k : keys)
                        {
                            nodeIDs.emplace_back(calcNodeID(k));
                            unl.emplace(nodeIDs.back());
                        }

                        std::uint32_t nIdx = 0;
                        for (auto score : scores)
                        {
                            scoreTable[nodeIDs[nIdx++]] = score;
                            scoreTable[nodeIDs[nIdx++]] = score;
                        }
                        for (; nIdx < unl_size;)
                        {
                            scoreTable[nodeIDs[nIdx++]] = scores.back();
                        }

                        if (nUnl_percent == 100)
                        {
                            negUnl = unl;
                        }
                        else if (nUnl_percent == 50)
                        {
                            for (std::uint32_t i = 1; i < unl_size; i += 2)
                                negUnl.insert(nodeIDs[i]);
                        }
                    };

                for (auto us : unlSizes)
                {
                    for (auto np : nUnlPercent)
                    {
                        hash_set<NodeID> unl;
                        hash_set<NodeID> negUnl;
                        hash_map<NodeID, std::uint32_t> scoreTable;

                        fillScoreTable(us, np, unl, negUnl, scoreTable);
                        BEAST_EXPECT(unl.size() == us);
                        BEAST_EXPECT(negUnl.size() == us * np / 100);
                        BEAST_EXPECT(scoreTable.size() == us);

                        std::size_t toDisable_expect = 0;
                        std::size_t toReEnable_expect = 0;
                        if (np == 0)
                        {
                            toDisable_expect = 4;
                        }
                        else if (np == 50)
                        {
                            toReEnable_expect = negUnl.size() - 6;
                        }
                        else
                        {
                            toReEnable_expect = negUnl.size() - 12;
                        }
                        BEAST_EXPECT(checkCandidateSizes(
                            vote,
                            unl,
                            negUnl,
                            scoreTable,
                            toDisable_expect,
                            toReEnable_expect));
                    }
                }
            }
        }
    }

    void
    testNewValidators()
    {
        testcase("New Validators");
        jtx::Env env(*this);

        NodeID myId(0xA0);
        NegativeUNLVote vote(myId, env.journal);

        // test cases:
        // newValidators_ of the NegativeUNLVote empty, add one
        // add a new one and one already added
        // add a new one and some already added
        // purge and see some are expired

        NodeID n1(0xA1);
        NodeID n2(0xA2);
        NodeID n3(0xA3);

        vote.newValidators(2, {n1});
        BEAST_EXPECT(vote.newValidators_.size() == 1);
        if (vote.newValidators_.size() == 1)
        {
            BEAST_EXPECT(vote.newValidators_.begin()->first == n1);
            BEAST_EXPECT(vote.newValidators_.begin()->second == 2);
        }

        vote.newValidators(3, {n1, n2});
        BEAST_EXPECT(vote.newValidators_.size() == 2);
        if (vote.newValidators_.size() == 2)
        {
            BEAST_EXPECT(vote.newValidators_[n1] == 2);
            BEAST_EXPECT(vote.newValidators_[n2] == 3);
        }

        vote.newValidators(
            NegativeUNLVote::newValidatorDisableSkip, {n1, n2, n3});
        BEAST_EXPECT(vote.newValidators_.size() == 3);
        if (vote.newValidators_.size() == 3)
        {
            BEAST_EXPECT(vote.newValidators_[n1] == 2);
            BEAST_EXPECT(vote.newValidators_[n2] == 3);
            BEAST_EXPECT(
                vote.newValidators_[n3] ==
                NegativeUNLVote::newValidatorDisableSkip);
        }

        vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 2);
        BEAST_EXPECT(vote.newValidators_.size() == 3);
        vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 3);
        BEAST_EXPECT(vote.newValidators_.size() == 2);
        vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 4);
        BEAST_EXPECT(vote.newValidators_.size() == 1);
        BEAST_EXPECT(vote.newValidators_.begin()->first == n3);
        BEAST_EXPECT(
            vote.newValidators_.begin()->second ==
            NegativeUNLVote::newValidatorDisableSkip);
    }

    void
    run() override
    {
        testAddTx();
        testPickOneCandidate();
        testBuildScoreTableSpecialCases();
        testFindAllCandidates();
        testFindAllCandidatesCombination();
        testNewValidators();
    }
};

/**
 * Rest the build score table function of NegativeUNLVote.
 * This was a part of NegativeUNLVoteInternal. It is redundant and has long
 * runtime. So we separate it out as a manual test.
 */
class NegativeUNLVoteScoreTable_test : public beast::unit_test::suite
{
    void
    testBuildScoreTableCombination()
    {
        testcase("Build Score Table Combination");
        /*
         * local node good history, correct scores:
         * == combination:
         * -- unl size: 10, 34, 35, 50
         * -- score pattern: all 0, all 50%, all 100%, two 0% two 50% rest 100%
         */
        std::array<std::uint32_t, 4> unlSizes = {10, 34, 35, 50};
        std::array<std::array<std::uint32_t, 3>, 4> scorePattern = {
            {{{0, 0, 0}}, {{50, 50, 50}}, {{100, 100, 100}}, {{0, 50, 100}}}};

        for (auto unlSize : unlSizes)
        {
            for (std::uint32_t sp = 0; sp < 4; ++sp)
            {
                NetworkHistory history = {
                    *this, {unlSize, 0, false, false, 256 + 2}};
                BEAST_EXPECT(history.goodHistory);
                if (history.goodHistory)
                {
                    NodeID myId = history.UNLNodeIDs[3];
                    history.walkHistoryAndAddValidations(
                        [&](std::shared_ptr<Ledger const> const& l,
                            std::size_t idx) -> bool {
                            std::size_t k;
                            if (idx < 2)
                                k = 0;
                            else if (idx < 4)
                                k = 1;
                            else
                                k = 2;

                            bool add_50 =
                                scorePattern[sp][k] == 50 && l->seq() % 2 == 0;
                            bool add_100 = scorePattern[sp][k] == 100;
                            bool add_me = history.UNLNodeIDs[idx] == myId;
                            return add_50 || add_100 || add_me;
                        });

                    NegativeUNLVote vote(myId, history.env.journal);
                    auto scoreTable = vote.buildScoreTable(
                        history.lastLedger(),
                        history.UNLNodeIDSet,
                        history.validations);
                    BEAST_EXPECT(scoreTable);
                    if (scoreTable)
                    {
                        std::uint32_t i = 0;  // looping unl
                        auto checkScores = [&](std::uint32_t score,
                                               std::uint32_t k) -> bool {
                            if (history.UNLNodeIDs[i] == myId)
                                return score == 256;
                            if (scorePattern[sp][k] == 0)
                                return score == 0;
                            if (scorePattern[sp][k] == 50)
                                return score == 256 / 2;
                            if (scorePattern[sp][k] == 100)
                                return score == 256;
                            else
                                return false;
                        };
                        for (; i < 2; ++i)
                        {
                            BEAST_EXPECT(checkScores(
                                (*scoreTable)[history.UNLNodeIDs[i]], 0));
                        }
                        for (; i < 4; ++i)
                        {
                            BEAST_EXPECT(checkScores(
                                (*scoreTable)[history.UNLNodeIDs[i]], 1));
                        }
                        for (; i < unlSize; ++i)
                        {
                            BEAST_EXPECT(checkScores(
                                (*scoreTable)[history.UNLNodeIDs[i]], 2));
                        }
                    }
                }
            }
        }
    }

    void
    run() override
    {
        testBuildScoreTableCombination();
    }
};

/*
 * Test the doVoting function of NegativeUNLVote.
 * The test cases are split to 5 classes for parallel execution.
 *
 * Voting tests: (use hasToDisable and hasToReEnable in some of the cases)
 *
 * == all good score, nUnl empty
 * -- txSet.size = 0
 * == all good score, nUnl not empty (use hasToDisable)
 * -- txSet.size = 1
 *
 * == 2 nodes offline, nUnl empty (use hasToReEnable)
 * -- txSet.size = 1
 * == 2 nodes offline, in nUnl
 * -- txSet.size = 0
 *
 * == 2 nodes offline, not in nUnl, but maxListed
 * -- txSet.size = 0
 *
 * == 2 nodes offline including me, not in nUnl
 * -- txSet.size = 0
 * == 2 nodes offline, not in negativeUNL, but I'm not a validator
 * -- txSet.size = 0
 * == 2 in nUnl, but not in unl, no other remove candidates
 * -- txSet.size = 1
 *
 * == 2 new validators have bad scores
 * -- txSet.size = 0
 * == 2 expired new validators have bad scores
 * -- txSet.size = 1
 */

class NegativeUNLVoteGoodScore_test : public beast::unit_test::suite
{
    void
    testDoVoting()
    {
        testcase("Do Voting");

        {
            //== all good score, negativeUNL empty
            //-- txSet.size = 0
            NetworkHistory history = {*this, {51, 0, false, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return true; });
                BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 0));
            }
        }

        {
            // all good score, negativeUNL not empty (use hasToDisable)
            //-- txSet.size = 1
            NetworkHistory history = {*this, {37, 0, true, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return true; });
                BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 1));
            }
        }
    }

    void
    run() override
    {
        testDoVoting();
    }
};

class NegativeUNLVoteOffline_test : public beast::unit_test::suite
{
    void
    testDoVoting()
    {
        testcase("Do Voting");

        {
            //== 2 nodes offline, negativeUNL empty (use hasToReEnable)
            //-- txSet.size = 1
            NetworkHistory history = {*this, {29, 1, false, true, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool {
                        // skip node 0 and node 1
                        return idx > 1;
                    });
                BEAST_EXPECT(
                    voteAndCheck(history, history.UNLNodeIDs.back(), 1));
            }
        }

        {
            // 2 nodes offline, in negativeUNL
            //-- txSet.size = 0
            NetworkHistory history = {*this, {30, 1, true, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                NodeID n1 =
                    calcNodeID(*history.lastLedger()->negativeUNL().begin());
                NodeID n2 =
                    calcNodeID(*history.lastLedger()->validatorToDisable());
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool {
                        // skip node 0 and node 1
                        return history.UNLNodeIDs[idx] != n1 &&
                            history.UNLNodeIDs[idx] != n2;
                    });
                BEAST_EXPECT(
                    voteAndCheck(history, history.UNLNodeIDs.back(), 0));
            }
        }
    }

    void
    run() override
    {
        testDoVoting();
    }
};

class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite
{
    void
    testDoVoting()
    {
        testcase("Do Voting");

        {
            // 2 nodes offline, not in negativeUNL, but maxListed
            //-- txSet.size = 0
            NetworkHistory history = {*this, {32, 8, true, true, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool {
                        // skip node 0 ~ 10
                        return idx > 10;
                    });
                BEAST_EXPECT(
                    voteAndCheck(history, history.UNLNodeIDs.back(), 0));
            }
        }
    }

    void
    run() override
    {
        testDoVoting();
    }
};

class NegativeUNLVoteRetiredValidator_test : public beast::unit_test::suite
{
    void
    testDoVoting()
    {
        testcase("Do Voting");

        {
            //== 2 nodes offline including me, not in negativeUNL
            //-- txSet.size = 0
            NetworkHistory history = {*this, {35, 0, false, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return idx > 1; });
                BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 0));
            }
        }

        {
            // 2 nodes offline, not in negativeUNL, but I'm not a validator
            //-- txSet.size = 0
            NetworkHistory history = {*this, {40, 0, false, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return idx > 1; });
                BEAST_EXPECT(voteAndCheck(history, NodeID(0xdeadbeef), 0));
            }
        }

        {
            //== 2 in negativeUNL, but not in unl, no other remove candidates
            //-- txSet.size = 1
            NetworkHistory history = {*this, {25, 2, false, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return idx > 1; });
                BEAST_EXPECT(voteAndCheck(
                    history,
                    history.UNLNodeIDs.back(),
                    1,
                    [&](NegativeUNLVote& vote) {
                        history.UNLKeySet.erase(history.UNLKeys[0]);
                        history.UNLKeySet.erase(history.UNLKeys[1]);
                    }));
            }
        }
    }

    void
    run() override
    {
        testDoVoting();
    }
};

class NegativeUNLVoteNewValidator_test : public beast::unit_test::suite
{
    void
    testDoVoting()
    {
        testcase("Do Voting");

        {
            //== 2 new validators have bad scores
            //-- txSet.size = 0
            NetworkHistory history = {*this, {15, 0, false, false, {}}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return true; });
                BEAST_EXPECT(voteAndCheck(
                    history,
                    history.UNLNodeIDs[0],
                    0,
                    [&](NegativeUNLVote& vote) {
                        auto extra_key_1 =
                            randomKeyPair(KeyType::ed25519).first;
                        auto extra_key_2 =
                            randomKeyPair(KeyType::ed25519).first;
                        history.UNLKeySet.insert(extra_key_1);
                        history.UNLKeySet.insert(extra_key_2);
                        hash_set<NodeID> nowTrusted;
                        nowTrusted.insert(calcNodeID(extra_key_1));
                        nowTrusted.insert(calcNodeID(extra_key_2));
                        vote.newValidators(
                            history.lastLedger()->seq(), nowTrusted);
                    }));
            }
        }

        {
            //== 2 expired new validators have bad scores
            //-- txSet.size = 1
            NetworkHistory history = {
                *this,
                {21,
                 0,
                 false,
                 false,
                 NegativeUNLVote::newValidatorDisableSkip * 2}};
            BEAST_EXPECT(history.goodHistory);
            if (history.goodHistory)
            {
                history.walkHistoryAndAddValidations(
                    [&](std::shared_ptr<Ledger const> const& l,
                        std::size_t idx) -> bool { return true; });
                BEAST_EXPECT(voteAndCheck(
                    history,
                    history.UNLNodeIDs[0],
                    1,
                    [&](NegativeUNLVote& vote) {
                        auto extra_key_1 =
                            randomKeyPair(KeyType::ed25519).first;
                        auto extra_key_2 =
                            randomKeyPair(KeyType::ed25519).first;
                        history.UNLKeySet.insert(extra_key_1);
                        history.UNLKeySet.insert(extra_key_2);
                        hash_set<NodeID> nowTrusted;
                        nowTrusted.insert(calcNodeID(extra_key_1));
                        nowTrusted.insert(calcNodeID(extra_key_2));
                        vote.newValidators(256, nowTrusted);
                    }));
            }
        }
    }

    void
    run() override
    {
        testDoVoting();
    }
};

class NegativeUNLVoteFilterValidations_test : public beast::unit_test::suite
{
    void
    testFilterValidations()
    {
        testcase("Filter Validations");
        jtx::Env env(*this);
        auto l = std::make_shared<Ledger>(
            create_genesis,
            env.app().config(),
            std::vector<uint256>{},
            env.app().getNodeFamily());

        auto createSTVal = [&](std::pair<PublicKey, SecretKey> const& keys) {
            return std::make_shared<STValidation>(
                env.app().timeKeeper().now(),
                keys.first,
                keys.second,
                calcNodeID(keys.first),
                [&](STValidation& v) {
                    v.setFieldH256(sfLedgerHash, l->info().hash);
                    v.setFieldU32(sfLedgerSequence, l->seq());
                    v.setFlag(vfFullValidation);
                });
        };

        // create keys and validations
        std::uint32_t numNodes = 10;
        std::uint32_t negUnlSize = 3;
        std::vector<std::string> cfgKeys;
        hash_set<NodeID> activeValidators;
        hash_set<PublicKey> nUnlKeys;
        std::vector<std::shared_ptr<STValidation>> vals;
        for (int i = 0; i < numNodes; ++i)
        {
            auto keyPair = randomKeyPair(KeyType::secp256k1);
            vals.emplace_back(createSTVal(keyPair));
            cfgKeys.push_back(toBase58(TokenType::NodePublic, keyPair.first));
            activeValidators.emplace(calcNodeID(keyPair.first));
            if (i < negUnlSize)
            {
                nUnlKeys.insert(keyPair.first);
            }
        }

        // setup the ValidatorList
        auto& validators = env.app().validators();
        auto& local = *nUnlKeys.begin();
        std::vector<std::string> cfgPublishers;
        validators.load(local, cfgKeys, cfgPublishers);
        validators.updateTrusted(
            activeValidators,
            env.timeKeeper().now(),
            env.app().getOPs(),
            env.app().overlay(),
            env.app().getHashRouter());
        BEAST_EXPECT(validators.getTrustedMasterKeys().size() == numNodes);
        validators.setNegativeUNL(nUnlKeys);
        BEAST_EXPECT(validators.getNegativeUNL().size() == negUnlSize);

        // test the filter
        BEAST_EXPECT(vals.size() == numNodes);
        vals = validators.negativeUNLFilter(std::move(vals));
        BEAST_EXPECT(vals.size() == numNodes - negUnlSize);
    }

    void
    run() override
    {
        testFilterValidations();
    }
};

BEAST_DEFINE_TESTSUITE(NegativeUNL, consensus, ripple);

BEAST_DEFINE_TESTSUITE(NegativeUNLVoteInternal, consensus, ripple);
BEAST_DEFINE_TESTSUITE_MANUAL(NegativeUNLVoteScoreTable, consensus, ripple);
BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteGoodScore, consensus, ripple, 1);
BEAST_DEFINE_TESTSUITE(NegativeUNLVoteOffline, consensus, ripple);
BEAST_DEFINE_TESTSUITE(NegativeUNLVoteMaxListed, consensus, ripple);
BEAST_DEFINE_TESTSUITE_PRIO(
    NegativeUNLVoteRetiredValidator,
    consensus,
    ripple,
    1);
BEAST_DEFINE_TESTSUITE(NegativeUNLVoteNewValidator, consensus, ripple);
BEAST_DEFINE_TESTSUITE(NegativeUNLVoteFilterValidations, consensus, ripple);

///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
bool
negUnlSizeTest(
    std::shared_ptr<Ledger const> const& l,
    size_t size,
    bool hasToDisable,
    bool hasToReEnable)
{
    bool sameSize = l->negativeUNL().size() == size;
    bool sameToDisable =
        (l->validatorToDisable() != std::nullopt) == hasToDisable;
    bool sameToReEnable =
        (l->validatorToReEnable() != std::nullopt) == hasToReEnable;

    return sameSize && sameToDisable && sameToReEnable;
}

bool
applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass)
{
    auto const res =
        apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal);
    if (pass)
        return res.ter == tesSUCCESS;
    else
        return res.ter == tefFAILURE || res.ter == temDISABLED;
}

bool
VerifyPubKeyAndSeq(
    std::shared_ptr<Ledger const> const& l,
    hash_map<PublicKey, std::uint32_t> nUnlLedgerSeq)
{
    auto sle = l->read(keylet::negativeUNL());
    if (!sle)
        return false;
    if (!sle->isFieldPresent(sfDisabledValidators))
        return false;

    auto const& nUnlData = sle->getFieldArray(sfDisabledValidators);
    if (nUnlData.size() != nUnlLedgerSeq.size())
        return false;

    for (auto const& n : nUnlData)
    {
        if (!n.isFieldPresent(sfFirstLedgerSequence) ||
            !n.isFieldPresent(sfPublicKey))
            return false;

        auto seq = n.getFieldU32(sfFirstLedgerSequence);
        auto d = n.getFieldVL(sfPublicKey);
        auto s = makeSlice(d);
        if (!publicKeyType(s))
            return false;
        PublicKey pk(s);
        auto it = nUnlLedgerSeq.find(pk);
        if (it == nUnlLedgerSeq.end())
            return false;
        if (it->second != seq)
            return false;
        nUnlLedgerSeq.erase(it);
    }
    return nUnlLedgerSeq.size() == 0;
}

std::size_t
countTx(std::shared_ptr<SHAMap> const& txSet)
{
    std::size_t count = 0;
    for (auto i = txSet->begin(); i != txSet->end(); ++i)
    {
        ++count;
    }
    return count;
};

std::vector<PublicKey>
createPublicKeys(std::size_t n)
{
    std::vector<PublicKey> keys;
    std::size_t ss = 33;
    std::vector<uint8_t> data(ss, 0);
    data[0] = 0xED;
    for (int i = 0; i < n; ++i)
    {
        data[1]++;
        Slice s(data.data(), ss);
        keys.emplace_back(s);
    }
    return keys;
}

STTx
createTx(bool disabling, LedgerIndex seq, PublicKey const& txKey)
{
    auto fill = [&](auto& obj) {
        obj.setFieldU8(sfUNLModifyDisabling, disabling ? 1 : 0);
        obj.setFieldU32(sfLedgerSequence, seq);
        obj.setFieldVL(sfUNLModifyValidator, txKey);
    };
    return STTx(ttUNL_MODIFY, fill);
}

}  // namespace test
}  // namespace ripple
